package com.athena.framework.url.route;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.io.resource.NoResourceException;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONUtil;
import com.athena.config.DefaultAppConfig;
import com.athena.framework.annotation.ConvertHandle;
import com.athena.framework.annotation.ResponseBody;
import com.athena.framework.authority.Security;
import com.athena.framework.config.ConversionCache;
import com.athena.framework.config.DataConversion;
import com.athena.framework.config.HttpContentType;
import com.athena.framework.exception.BeanException;
import com.athena.framework.exception.DataConvertException;
import com.athena.framework.exception.UrlException;
import com.athena.framework.url.info.UrlInfo;
import com.athena.framework.url.info.UrlInfoFactory;
import com.athena.util.MethodUtil;
import org.apache.catalina.connector.RequestFacade;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.FilterChain;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 路由中心，分发各个url
 *
 * @author likun
 */
public class RouteCenter {

    private final static Logger logger = LoggerFactory.getLogger(RouteCenter.class);

    private static final int MEMORY_THRESHOLD = 1024 * 1024 * 3;
    private static final long MAX_FILE_SIZE = 1024 * 1024 * 1024 * 10L;
    private static final long MAX_REQUEST_SIZE = 1024 * 1024 * 1024 * 50L;

    /**
     * 根据url分发连接
     *
     * @param request
     * @param response
     * @param chain
     */
    public void route(ServletRequest request, ServletResponse response, FilterChain chain) {
        String url = ((RequestFacade) request).getPathInfo();
        //验证权限
        if (hasAuthority(url, request, response)) {
            UrlInfo urlInfo = UrlInfoFactory.creatUrlInfoFactory().getUrlInfoByUrl(url);
            if (urlInfo == null || !(urlInfo.getExecutor() instanceof Method)) {
                //查看是否有对应的文件,如果有，那么就响应相应的文件并返回true，没有就返回false
                if (!hasFile(url, request, response)) {
                    //没有找到url路径，返回404
                    return404(request, response);
                }
            } else {
                exeMethod(urlInfo, request, response, chain);
            }
        } else {
            return403(request, response);
        }
    }

    /**
     * 返回无权限页面
     *
     * @param request
     * @param response
     */
    private void return403(ServletRequest request, ServletResponse response) {
        returnErrorPage(403, request, response);
    }

    /**
     * 验证权限
     *
     * @param url
     * @param request
     * @param response
     * @return
     */
    private boolean hasAuthority(String url, ServletRequest request, ServletResponse response) {
        return Security.hasAuthority(url, request);
    }

    /**
     * 查看文件夹中是否有对应的资源文件，如果有，那么就直接响应资源文件
     *
     * @param url
     * @param request
     * @param response
     * @return
     */
    private boolean hasFile(String url, ServletRequest request, ServletResponse response) {
        //判断文件是否存在
        File file;
        if ((file = fileExists(url)) != null) {
            //设置请求响应的内容类型
            setContextType(url, response);
            try {
                FileInputStream fis = new FileInputStream(file);
                IoUtil.copy(fis, response.getOutputStream(), IoUtil.DEFAULT_BUFFER_SIZE);
                fis.close();
            } catch (FileNotFoundException e) {
                String msg = "找不到静态资源：" + DefaultAppConfig.webDir + url + "\r\n" + e.getMessage();
                logger.error(msg);
                throw new UrlException(msg);
            } catch (IOException e) {
                String msg = "获取响应输出流失败。\r\n" + e.getMessage();
                logger.error(msg);
                throw new UrlException(msg);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * 设置请求响应的内容类型
     *
     * @param url
     * @param response
     */
    private void setContextType(String url, ServletResponse response) {
        //获取文件后缀
        if (url.lastIndexOf(".") != -1) {
            String suffix = url.substring(url.lastIndexOf("."));
            String contextType = HttpContentType.get(suffix);
            if (!StrUtil.isBlank(contextType)) {
                response.setContentType(contextType);
            }
        }
    }

    /**
     * 判断是否存在url指定的文件
     *
     * @param url
     * @return
     */
    public File fileExists(String url) {
        if (DefaultAppConfig.webInClassPath) {
            try {
                ClassPathResource resource = new ClassPathResource(DefaultAppConfig.webDir + url);
                return resource.getFile();
            } catch (NoResourceException e) {
                return null;
            }
        } else {
            File file = new File(DefaultAppConfig.webDir + url);
            return file.exists() ? file : null;
        }
    }

    /**
     * 执行URL映射的方法
     *
     * @param urlInfo  url信息
     * @param request
     * @param response
     * @param chain
     */
    private void exeMethod(UrlInfo urlInfo, ServletRequest request, ServletResponse response, FilterChain chain) {
        //获取url映射方法的参数
        Method method = (Method) urlInfo.getExecutor();
        Class<?>[] types = method.getParameterTypes();
        //获取方法的参数名称
        String[] parameterNames = MethodUtil.parameterName(method);
        Object[] parm = new Object[parameterNames.length];
        Map<String, String> paramMap = ServletUtil.getParamMap(request);
        for (int i = 0; i < types.length; i++) {
            if (types[i].isAssignableFrom(request.getClass())) {
                parm[i] = request;
            } else if (types[i].isAssignableFrom(response.getClass())) {
                parm[i] = response;
            } else if (types[i].isAssignableFrom(FileItem[].class)) {
                //如果是文件上传，那么就解析并设置文件数据
                setFileUploadParm((HttpServletRequest) request, parm, i);
            } else {
                String value = paramMap.get(parameterNames[i]);
                //判断是否有ConvertHandle注解
                ConvertHandle annotation = method.getParameters()[i].getAnnotation(ConvertHandle.class);
                //如果有ConvertHandle注解，那么就调用ConvertHandle注解中的转换器转换数据并为参数设置值
                if (annotation != null) {
                    Class<? extends DataConversion> conversion = annotation.value();
                    //先在缓存中找,如果找不到就新建一个对象,并存入缓存
                    DataConversion dataConversion = ConversionCache.get(conversion.getName());
                    if (dataConversion == null) {
                        try {
                            dataConversion = conversion.newInstance();
                            ConversionCache.put(conversion.getName(), dataConversion);
                        } catch (Exception e) {
                            String msg = "创建转换器：" + conversion.getName() + "失败。\r\n" + e.getMessage();
                            logger.error(msg);
                            throw new BeanException(msg);
                        }
                    }
                    parm[i] = dataConversion.conversion(value, types[i]);
                } else {
                    //如果没有转换注解，那么就检查是否是基本数据类型，如果是的话就默认的转换
                    if (value != null) {
                        // 为参数设置值
                        setParamValue(types[i], parm, i, value);
                    } else {
                        //设置参数的默认值
                        setParamDefaultValue(types[i], parameterNames, parm, paramMap, i);
                    }
                }
            }
        }
        try {
            Object invokeData = method.invoke(urlInfo.getParent(), parm);
            //处理执行方法返回的结果
            workingData(invokeData, request, response, method);
        } catch (Exception e) {
            String msg = "执行方法失败：" + method.getDeclaringClass().getName() + ":" + method.getName() + "\r\n" + e.getMessage();
            logger.error(msg);
            throw new BeanException(msg);
        }
    }

    /**
     * 为参数设置值
     *
     * @param type
     * @param parm
     * @param i
     * @param value
     */
    private void setParamValue(Class<?> type, Object[] parm, int i, String value) {
        //判断是否是基础数据类型,如果是基本类型，那么就转换类型
        if (type.getName().equals("byte") || type == Byte.class) {
            parm[i] = Byte.valueOf(value);
        } else if (type.getName().equals("char") || type == Character.class) {
            if (value.length() > 1) {
                String msg = "String转" + type.getName() + "失败,\"" + value + "\"的长度大于1.";
                logger.error(msg);
                throw new DataConvertException(msg);
            }
            parm[i] = Character.charCount(value.charAt(0));
        } else if (type.getName().equals("short") || type == Short.class) {
            parm[i] = Short.valueOf(value);
        } else if (type.getName().equals("int") || type == Integer.class) {
            parm[i] = Integer.valueOf(value);
        } else if (type.getName().equals("long") || type == Long.class) {
            parm[i] = Long.valueOf(value);
        } else if (type.getName().equals("float") || type == Float.class) {
            parm[i] = Float.valueOf(value);
        } else if (type.getName().equals("double") || type == Double.class) {
            parm[i] = Float.valueOf(value);
        } else if (type.getName().equals("boolean") || type == Boolean.class) {
            parm[i] = Boolean.valueOf(value);
        } else if (type == String.class) {
            parm[i] = value;
        } else if (type == Date.class) {
            parm[i] = DateUtil.parse(value);
        } else {
            //如果上面的类型都不是，那么为空
            parm[i] = null;
        }
    }

    /**
     * 设置参数的默认值
     *
     * @param type
     * @param parameterNames
     * @param parm
     * @param paramMap
     * @param i
     */
    private void setParamDefaultValue(Class<?> type, String[] parameterNames, Object[] parm, Map<String, String> paramMap, int i) {
        if (type.getName().equals("byte")) {
            parm[i] = 0;
        } else if (type.getName().equals("char")) {
            parm[i] = 0;
        } else if (type.getName().equals("short")) {
            parm[i] = 0;
        } else if (type.getName().equals("int")) {
            parm[i] = 0;
        } else if (type.getName().equals("long")) {
            parm[i] = 0;
        } else if (type.getName().equals("float")) {
            parm[i] = 0f;
        } else if (type.getName().equals("double")) {
            parm[i] = 0d;
        } else if (type.getName().equals("boolean")) {
            parm[i] = false;
        } else {
            //如果上面的类型都不是,那么尝试将所有的参数转换成bean
            try {
                parm[i] = fillBean(type, paramMap, parameterNames[i]);
            } catch (Exception e) {
                parm[i] = null;
            }
        }
    }

    /**
     * 如果是文件上传，那么就解析并设置文件数据
     *
     * @param request
     * @param parm
     * @param i
     */
    private void setFileUploadParm(HttpServletRequest request, Object[] parm, int i) {
        // 检测是否为多媒体上传
        if (!ServletFileUpload.isMultipartContent(request)) {
            parm[i] = null;
        }
        // 配置上传参数
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 设置内存临界值 - 超过后将产生临时文件并存储于临时目录中
        factory.setSizeThreshold(MEMORY_THRESHOLD);
        // 设置临时存储目录
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));

        ServletFileUpload upload = new ServletFileUpload(factory);
        // 设置最大文件上传值
        upload.setFileSizeMax(MAX_FILE_SIZE);
        // 设置最大请求值 (包含文件和表单数据)
        upload.setSizeMax(MAX_REQUEST_SIZE);
        // 中文处理
        upload.setHeaderEncoding("UTF-8");
        // 解析请求的内容提取文件数据
        try {
            List<FileItem> formItems = upload.parseRequest(request);
            parm[i] = ArrayUtil.toArray(formItems, FileItem.class);
        } catch (FileUploadException e) {
            throw new UrlException(e.getMessage());
        }
    }

    /**
     * 处理controller方法执行后返回的结果
     *
     * @param invokeData
     * @param request
     * @param response
     * @param method
     */
    private void workingData(Object invokeData, ServletRequest request, ServletResponse response, Method method) {
        ServletOutputStream os;
        try {
            os = response.getOutputStream();
            if (invokeData == null) {
                //如果返回的数据是null，那么就直接关闭响应输出流，不响应任何数据
                os.close();
            } else {
                ResponseBody annotation = method.getAnnotation(ResponseBody.class);
                if (annotation == null) {//返回html
                    if (invokeData instanceof String) {
                        //如果返回的是字符串，那么就表示要读取web静态资源文件
                        //若找到了静态资源文件，那么就返回静态资源文件，没有就返回404状态
                        if (!hasFile(invokeData.toString(), request, response)) {
                            return404(request, response);
                        }
                    } else {//如果没有加ResponseBody注解，返回的结果也不是String，那么就调用返回结果的toString方法返回
                        try {
                            os.write(invokeData.toString().getBytes());
                        } catch (IOException e) {
                            String msg = "获取响应输出流失败。\r\n" + e.getMessage();
                            logger.error(msg);
                            throw new UrlException(msg);
                        }
                    }
                } else {//返回json
                    response.setContentType("application/json");
                    os.write(JSONUtil.toJsonStr(invokeData).getBytes());
                }
            }
        } catch (IOException e) {
            String msg = "获取响应输出流失败。\r\n" + e.getMessage();
            logger.error(msg);
            throw new UrlException(msg);
        }
    }

    /**
     * 将request中的参数转换成参数
     *
     * @param type          最终的数据类型
     * @param paramMap      请求携带的参数
     * @param parameterName 变量名
     */
    private <T> T fillBean(Class<T> type, Map<String, String> paramMap, String parameterName) {
        return BeanUtil.mapToBean(paramMap, type, false);
    }


    /**
     * 返回404状态
     *
     * @param request
     * @param response
     */
    private void return404(ServletRequest request, ServletResponse response) {
        returnErrorPage(404, request, response);
    }

    /**
     * 返回错误页面
     *
     * @param code
     * @param request
     * @param response
     */
    private void returnErrorPage(int code, ServletRequest request, ServletResponse response) {
        boolean showErrorPage = false;
        String url = null, msg = null;
        switch (code) {
            case 404:
                showErrorPage = !StrUtil.isEmpty(DefaultAppConfig.page_404);
                url = DefaultAppConfig.page_404;
                msg = "未找到资源";
                break;
            case 403:
                showErrorPage = !StrUtil.isEmpty(DefaultAppConfig.page_403);
                url = DefaultAppConfig.page_403;
                msg = "无权限访问该资源";
                break;
        }
        if (showErrorPage) {
            ((HttpServletResponse) response).setStatus(code);
            hasFile(url, request, response);
        } else {
            try {
                ((HttpServletResponse) response).sendError(code, msg);
            } catch (IOException e) {
                logger.error(e.getMessage());
            }
        }
    }
}
